﻿using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using ImageCropper.Lib;
using System.Collections.Generic;
using System.Linq;

namespace ImageCropper
{
    public class ResizableRectangle : UserControl
    {
        public event EventHandler<SnappedToEdgeEventArgs> SnappedToEdge;

        public ResizableRectangle()
        {
            this.MouseLeftButtonDown += new MouseButtonEventHandler(ResizableRectangle_MouseLeftButtonDown);
            this.MouseLeftButtonUp += new MouseButtonEventHandler(ResizableRectangle_MouseLeftButtonUp);
            this.SizeChanged += new SizeChangedEventHandler(ResizableRectangle_SizeChanged);
            this.LostMouseCapture += new MouseEventHandler(ResizableRectangle_LostMouseCapture);
        }        
        
        private bool _isLeftButtonDown = false;
        private Point? _mouseDownPos = null;
        private Point? _thisPos = null;
        private Size? _thisSize = null;
        private UIElement _elementWithMouse;
        private double? _widthToHeightRatio = null;
        private bool _modifyHeight;
        private int _ratioSemaphore = 0;

        public double Top
        {
            get { return (double)this.GetValue(Canvas.TopProperty); }
            set { this.SetValue(Canvas.TopProperty, value); }
        }
        public double Left
        {
            get { return (double)this.GetValue(Canvas.LeftProperty); }
            set { this.SetValue(Canvas.LeftProperty, value); }
        }
        public IEnumerable<Edge> Edges
        {
            get
            {
                double bottom = this.Top + this.Height;
                double right = this.Left + this.Width;

                return new Edge[]
                    {
                        new Edge(Direction.West, this.Left, this.Top, bottom),
                        new Edge(Direction.North, this.Top, this.Left, right),
                        new Edge(Direction.East, right, this.Top, bottom),
                        new Edge(Direction.South, bottom, this.Left, right)
                    };
            }
        }

        public CroppingSurface Surface { get; set; }

        protected virtual IEnumerable<ResizableRectangle> SnappableObjects
        {
            get { return Enumerable.Empty<ResizableRectangle>(); }
        }   

        protected void _ClearTemps()
        {
            _isLeftButtonDown = false;
            _mouseDownPos = null;
            _thisPos = null;
            _thisSize = null;
            _ratioSemaphore--;

            if (this.SnappedToEdge != null)
                this.SnappedToEdge(this, new SnappedToEdgeEventArgs() { Edges = new Edge[0] });
        }
        protected void _Move(MouseEventArgs e)
        {
            if (_isLeftButtonDown && _mouseDownPos != null && this.Parent is Canvas)
            {
                Point mousePos = e.GetPosition(this.Parent as Canvas);
                this.Left = mousePos.X - _mouseDownPos.Value.X;
                this.Top = mousePos.Y - _mouseDownPos.Value.Y;

                Tuple<Edge, Edge> closeEastWest;
                Tuple<Edge, Edge> closeNorthSouth;
                var closeEdges = GetCloseEdges(this.Edges.ToList());

                closeEastWest = closeEdges
                    .Where(r => r.Item1.Axis == Axis.EastWest)                    
                    .FirstOrDefault();
                closeNorthSouth = closeEdges
                    .Where(r => r.Item1.Axis == Axis.NorthSouth)                    
                    .FirstOrDefault();

                if (closeEastWest != null)
                    this.Top += closeEastWest.Item2.Position - closeEastWest.Item1.Position;
                
                if (closeNorthSouth != null)
                    this.Left += closeNorthSouth.Item2.Position - closeNorthSouth.Item1.Position;

                List<Edge> snappedEdges = new List<Edge>();
                if (closeEastWest != null)
                    snappedEdges.Add(closeEastWest.Item2);
                if (closeNorthSouth != null)
                    snappedEdges.Add(closeNorthSouth.Item2);

                if (this.SnappedToEdge != null)
                    this.SnappedToEdge(this, new SnappedToEdgeEventArgs() { Edges = snappedEdges });

            }
        }

        private List<Tuple<Edge, Edge>> GetCloseEdges(List<Edge> thisEdges)
        {
            List<Edge> otherEdges = this.SnappableObjects
                                    .Where(r => r != this)
                                    .SelectMany(r => r.Edges)
                                    .ToList();

            return thisEdges
                    .Join(
                        otherEdges,
                        r => r.Axis,
                        r => r.Axis,
                        (thisEdge, otherEdge) => new
                        {
                            thisEdge,
                            otherEdge,
                            Distance = Math.Abs(thisEdge.Position - otherEdge.Position),
                            Axis = thisEdge.Axis
                        })
                    .Where(r => r.Distance <= 5)
                    .OrderBy(r => r.Distance)
                    .Select(r => new Tuple<Edge, Edge>(r.thisEdge, r.otherEdge))
                    .ToList();
            
        }
        protected void _Resize(Direction dir, MouseEventArgs e)
        {
            if (_isLeftButtonDown)
            {
                _ratioSemaphore++;
                try
                {
                    Point mousePos = e.GetPosition(this.Parent as Canvas);
                    Point p = e.GetPosition(this);

                    double newLeft = (mousePos.X - _mouseDownPos.Value.X);
                    double newTop = (mousePos.Y - _mouseDownPos.Value.Y);

                    Tuple<Edge, Edge> closeEdge = null;

                    switch (dir)
                    {
                        case Direction.South:
                            this.Height = Math.Max(0, _thisSize.Value.Height + (p.Y - _mouseDownPos.Value.Y));
                            this.Width = this.Height * this._widthToHeightRatio.Value;
                            break;

                        case Direction.East:
                        case Direction.SouthEast:
                            this.Width = Math.Max(0, _thisSize.Value.Width + (p.X - _mouseDownPos.Value.X));
                            this.Height = this.Width / this._widthToHeightRatio.Value;
                            break;

                        case Direction.SouthWest:
                            this.Left = newLeft;
                            this.Width = Math.Max(0, _thisSize.Value.Width + (_thisPos.Value.X - newLeft));
                            this.Height = this.Width / this._widthToHeightRatio.Value;
                            break;

                        case Direction.West:
                            this.Left = newLeft;
                            this.Width = Math.Max(0, _thisSize.Value.Width + (_thisPos.Value.X - newLeft));
                            this.Height = this.Width / this._widthToHeightRatio.Value;
                            this.Top = _thisPos.Value.Y + (_thisSize.Value.Height - this.Width / this._widthToHeightRatio.Value);
                            break;

                        case Direction.North:
                        case Direction.NorthWest:
                            this.Top = newTop;
                            this.Height = Math.Max(0, _thisSize.Value.Height + (_thisPos.Value.Y - newTop));
                            this.Width = this.Height * this._widthToHeightRatio.Value;
                            this.Left = _thisPos.Value.X + (_thisSize.Value.Width - this.Height * this._widthToHeightRatio.Value);
                            break;

                        case Direction.NorthEast:
                            this.Top = newTop;
                            this.Height = Math.Max(0, _thisSize.Value.Height + (_thisPos.Value.Y - newTop));
                            this.Width = this.Height * this._widthToHeightRatio.Value;
                            break;
                    }


                    switch (dir)
                    {
                        case Direction.East:
                        case Direction.SouthEast:
                        case Direction.South:
                            closeEdge = GetCloseEdges(
                                this.Edges
                                    .Where(r => r.Direction == Direction.South || r.Direction == Direction.East)
                                    .ToList())
                                .FirstOrDefault();

                            if (closeEdge != null)
                            {
                                if (closeEdge.Item1.Axis == Axis.EastWest)
                                {
                                    this.Height = Math.Max(0, closeEdge.Item2.Position - _thisPos.Value.Y);
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                }
                                else if (closeEdge.Item1.Axis == Axis.NorthSouth)
                                {
                                    this.Height = Math.Max(0, (closeEdge.Item2.Position - _thisPos.Value.X) / this._widthToHeightRatio.Value);
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                }
                            }
                            break;

                        case Direction.NorthWest:
                        case Direction.North:
                        case Direction.West:
                            closeEdge = GetCloseEdges(
                                this.Edges
                                    .Where(r => r.Direction == Direction.West || r.Direction == Direction.North)
                                    .ToList())
                                .FirstOrDefault();

                            if (closeEdge != null)
                            {
                                if (closeEdge.Item1.Axis == Axis.EastWest)
                                {
                                    this.Top = closeEdge.Item2.Position;
                                    this.Height = _thisSize.Value.Height + (_thisPos.Value.Y - closeEdge.Item2.Position);
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                    this.Left = _thisPos.Value.X + (_thisSize.Value.Width - this.Height * this._widthToHeightRatio.Value);
                                }
                                else if (closeEdge.Item1.Axis == Axis.NorthSouth)
                                {
                                    this.Left = closeEdge.Item2.Position;
                                    this.Width = _thisSize.Value.Width + (_thisPos.Value.X - closeEdge.Item2.Position);
                                    this.Height = this.Width / this._widthToHeightRatio.Value;
                                    this.Top = _thisPos.Value.Y + (_thisSize.Value.Height - this.Width / this._widthToHeightRatio.Value);
                                }
                            }
                            break;

                        case Direction.SouthWest:
                            closeEdge = GetCloseEdges(
                                this.Edges
                                    .Where(r => r.Direction == Direction.South || r.Direction == Direction.West)
                                    .ToList())
                                .FirstOrDefault();

                            if (closeEdge != null)
                            {
                                if (closeEdge.Item1.Axis == Axis.EastWest)
                                {
                                    this.Height = closeEdge.Item2.Position - _thisPos.Value.Y;
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                    this.Left = _thisPos.Value.X + (_thisSize.Value.Width - this.Height * this._widthToHeightRatio.Value);
                                }
                                else if (closeEdge.Item1.Axis == Axis.NorthSouth)
                                {
                                    this.Left = closeEdge.Item2.Position;
                                    this.Width = _thisSize.Value.Width + (_thisPos.Value.X - closeEdge.Item2.Position);
                                    this.Height = this.Width / this._widthToHeightRatio.Value;                                    
                                }
                            }
                            break;

                        case Direction.NorthEast:
                            closeEdge = GetCloseEdges(
                                this.Edges
                                    .Where(r => r.Direction == Direction.North || r.Direction == Direction.East)
                                    .ToList())
                                .FirstOrDefault();

                            if (closeEdge != null)
                            {
                                if (closeEdge.Item1.Axis == Axis.EastWest)
                                {
                                    this.Top = closeEdge.Item2.Position;
                                    this.Height = _thisSize.Value.Height + (_thisPos.Value.Y - closeEdge.Item2.Position);
                                    this.Width = this.Height * this._widthToHeightRatio.Value;                                 
                                }
                                else if (closeEdge.Item1.Axis == Axis.NorthSouth)
                                {
                                    this.Width = closeEdge.Item2.Position - _thisPos.Value.X;
                                    this.Height = this.Width / this._widthToHeightRatio.Value;
                                    this.Top = _thisPos.Value.Y + (_thisSize.Value.Height - this.Width / this._widthToHeightRatio.Value);
                                }
                            }
                            break;
                    }

                    if (this.SnappedToEdge != null)
                    {
                        if (closeEdge != null)
                            this.SnappedToEdge(this, new SnappedToEdgeEventArgs() { Edges = new Edge[] { closeEdge.Item2 } });
                        else
                            this.SnappedToEdge(this, new SnappedToEdgeEventArgs() { Edges = new Edge[0] });
                    }
                }
                finally
                {
                    _ratioSemaphore--;
                }
            }
        }
        protected void _SetTemps(MouseButtonEventArgs e)
        {
            _isLeftButtonDown = true;
            _mouseDownPos = e.GetPosition(this);
            _thisPos = new Point(this.Left, this.Top);
            _thisSize = new Size(this.Width, this.Height);
            _modifyHeight = _mouseDownPos.Value.Y > _mouseDownPos.Value.X;
            _ratioSemaphore++;
        }      
        protected void _KeyDown(KeyEventArgs e, bool supportResize, bool supportMove)
        {
            double delta;
            if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control))
                delta = 20;
            else
                delta = 1;

            switch (Keyboard.Modifiers)
            {
                case ModifierKeys.None:
                case ModifierKeys.Control:
                    if (supportMove)
                    {
                        switch (e.Key)
                        {
                            case Key.Up:
                                this.Top -= delta;
                                break;
                            case Key.Down:
                                this.Top += delta;
                                break;
                            case Key.Left:
                                this.Left -= delta;
                                break;
                            case Key.Right:
                                this.Left += delta;
                                break;
                        }
                    }
                    break;

                case ModifierKeys.Shift:
                case ModifierKeys.Shift | ModifierKeys.Control:
                    if (supportResize)
                    {
                        _ratioSemaphore++;
                        try
                        {
                            switch (e.Key)
                            {
                                case Key.Up:
                                    this.Height -= delta;
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                    break;
                                case Key.Down:
                                    this.Height += delta;
                                    this.Width = this.Height * this._widthToHeightRatio.Value;
                                    break;
                                case Key.Left:
                                    this.Width -= delta;
                                    this.Height = this.Width / this._widthToHeightRatio.Value;
                                    break;
                                case Key.Right:
                                    this.Width += delta;
                                    this.Height = this.Width / this._widthToHeightRatio.Value;
                                    break;
                            }
                        }
                        finally
                        {
                            _ratioSemaphore--;
                        }
                    }
                    break;
            }
        }

        private void ResizableRectangle_LostMouseCapture(object sender, MouseEventArgs e)
        {
            _ClearTemps();
        }        
        private void ResizableRectangle_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;

            this.Focus();

            if (e.OriginalSource is UIElement)
                if ((e.OriginalSource as UIElement).CaptureMouse())
                    _elementWithMouse = e.OriginalSource as UIElement;

            _SetTemps(e);
        }
        private void ResizableRectangle_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            e.Handled = true;

            if (_elementWithMouse != null)
                _elementWithMouse.ReleaseMouseCapture();

            _ClearTemps();
        }
        private void ResizableRectangle_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (_ratioSemaphore == 0)
            {
                if (this.Height > 0 && this.Width > 0)
                    this._widthToHeightRatio = this.Width / this.Height;
                else
                    this._widthToHeightRatio = null;
            }
        }
    }

    public class SnappedToEdgeEventArgs : EventArgs
    {
        public IEnumerable<Edge> Edges { get; set; }
    }
}
